feat: SCAPI migration for jobs, code, and bm users/roles commands#413
Draft
clavery wants to merge 11 commits into
Draft
feat: SCAPI migration for jobs, code, and bm users/roles commands#413clavery wants to merge 11 commits into
clavery wants to merge 11 commits into
Conversation
Introduces a JobsBackend interface so job commands can transparently use either OCAPI or SCAPI. Auto mode prefers SCAPI when shortCode and tenantId are configured, falling back to OCAPI on invalid_scope errors. - New SCAPI Jobs client (operation/jobs/v1) with optimistic sfcc.jobs.rw scope and read-only downgrade for read operations - Canonical JobExecutionResult type bridges OCAPI snake_case and SCAPI camelCase response shapes - --api-backend flag and apiBackend dw.json field for explicit control - New job execution delete command (SCAPI only) - job:run, job:search, job:wait, job:log migrated to backend abstraction - job:import and job:export remain OCAPI-only for now
# Conflicts: # packages/b2c-cli/src/commands/job/run.ts # packages/b2c-cli/src/commands/job/search.ts # packages/b2c-tooling-sdk/src/config/dw-json.ts # packages/b2c-tooling-sdk/src/config/mapping.ts
Pulls three domain-agnostic utilities out of jobs into shared modules ahead of applying the same pattern to scripts, users, and roles: - isInvalidScopeError, ApiBackendPreference, resolveScapiOrOcapi — scope-error detection and preference resolution - ScapiFallbackBackend<T> — generic fallback wrapper that tries SCAPI first and falls back to OCAPI on invalid_scope - ScopeTierManager<C> — manages dual rw/read-only client tiers with optimistic rw + downgrade on scope error Refactors ScapiJobsBackend and FallbackJobsBackend to use the new utilities without behavior change. Removes the unused downgradeToReadOnly() method and confusing tier-resolution logic.
Migrates code list/activate/delete commands to use the dual-backend pattern. New CodeCommand base class exposes createScriptsBackend(), which selects between OCAPI and SCAPI based on --api-backend. - New SCAPI Scripts client (dx/scripts/v1) reusing the shared ScopeTierManager and ScapiFallbackBackend utilities - ScriptsBackend interface with canonical CodeVersionInfo shape (camelCase, _raw escape hatch) - reloadCodeVersion remains OCAPI-only — SCAPI backend throws, auto mode falls back to OCAPI on the first reload call
Migrates bm users list/get/update/delete commands to the dual-backend pattern. New BmCommand base class exposes createUsersBackend(), which selects between OCAPI and SCAPI based on --api-backend. - New SCAPI Merchant Users client (merchant/users/v1) - UsersBackend interface with canonical UserInfo (camelCase) - bm users search, bm whoami, bm access-key * stay OCAPI-only — no SCAPI equivalents - SCAPI updateUser does not support `disabled`; the SCAPI backend throws when --disabled is passed, prompting the user to use OCAPI
Migrates bm roles list/get/create/delete/grant/revoke and bm roles permissions get/set commands to the dual-backend pattern. BmCommand now exposes createRolesBackend() alongside createUsersBackend(). - New SCAPI Merchant Roles client (merchant/roles/v1) - RolesBackend with canonical RoleInfo and RolePermissionsInfo (camelCase; OCAPI mapping converts snake_case fields like locale_id → localeId) - Permissions display updated to use canonical camelCase fields - Bm roles get --expand users continues to work via the _raw escape hatch (handles both OCAPI snake_case and SCAPI camelCase user fields)
- Configuration guide: clarify api-backend applies to job, code, bm users, and bm roles commands - code.md: new "API Backend" section with SCAPI scopes, fallback behavior, and notes on reload/deploy/download/watch staying OCAPI/WebDAV - bm.md: new "API Backend" section with per-command compatibility table (users search, whoami, access-key remain OCAPI-only) - b2c-code skill: backend selection examples - b2c-bm-users-roles skill: backend selection notes including the --disabled fallback caveat - Single changeset replaces the jobs-only one
Three correctness fixes plus four DRY extractions across the SCAPI migration. Tests stay green (1722 SDK + 1219 CLI) and the API surface is unchanged for consumers. Bugs fixed: - code activate --reload now works in auto mode. The reload toggle (list + activate(alt) + activate(target)) is implementable on any backend, so reloadCodeVersion is a backend-agnostic free function that takes a ScriptsBackend. The OCAPI-only stub in ScapiScriptsBackend that previously broke fallback is gone. - job run --body is no longer subject to a special-case backend switch. SCAPI accepts raw bodies for system jobs (just with a slightly different payload shape) so we pass --body through to whichever backend the user picked. Removes a no-op resolveBackend helper that called createJobsBackend twice. - Scope merging now works for any AuthStrategy. The instanceof OAuthStrategy check silently dropped scopes for ImplicitOAuthStrategy and StatefulOAuthStrategy. AuthStrategy gains an optional withAdditionalScopes; a new withScopes() helper centralizes the method-presence check across all SCAPI client factories. DRY extractions: - Fallback*Backend subclasses (jobs, scripts, users, roles) replaced with a single Proxy-based createFallbackBackend<T>(). Each domain shed ~25 lines of mechanical method delegation. The proxy traps reads of `name` and routes method calls through the same withFallback logic. - create*Backend factory functions (jobs, scripts, users, roles) collapsed into createDualBackend<T>() that takes constructors. Each domain backend.ts shrunk from ~50 lines to ~10. - createScapi*Client factories collapsed into buildScapiClient<P>() that takes a path segment, scope set, and middleware key. Each domain client.ts shrunk from ~60 lines to ~30. - InstanceCommand gained createBackend<T>() so per-domain command base classes (JobCommand/CodeCommand/BmCommand) shrink to one-line wrappers. Other: - Renamed JobExecutionResult to JobExecutionInfo for consistency with CodeVersionInfo, UserInfo, RoleInfo across the canonical types.
Adds 8 unit tests for createFallbackBackend covering: - happy path (SCAPI works, choice is cached) - fallback path (invalid_scope triggers OCAPI, choice is cached) - name reflects the resolved backend - non-fallback errors are rethrown without falling back - multi-arg method dispatch - non-method property access (documented as SCAPI-target-only) Tightens the JSDoc on createFallbackBackend to spell out the contract explicitly: both backends must implement T (TypeScript enforces this at the call site), only methods are routed through fallback, non-method properties stay on the SCAPI target, and concurrent first-calls are benign since they only retry SCAPI redundantly.
The hostile review surfaced two real type-safety gaps in the dual-backend pattern. Both are fixed by making interface-level capability explicit rather than relying on runtime throws. 1. RoleInfo.permissions silently dropped after fallback. The OCAPI role mapper omitted permissions; SCAPI included them. Both satisfied the optional `permissions?` field, so TypeScript and the Proxy were happy while data quietly disappeared on the fallback path. Fix: map OCAPI's permissions through the existing snake_case → camelCase converter so getRole returns the same shape from both backends. 2. JobsBackend.deleteJobExecution was a runtime-throwing stub on OCAPI. Auto-mode behavior was unstable: it worked on a SCAPI-resolved backend, threw on an OCAPI-resolved one. Fix: split capability into DeletableJobsBackend (extends JobsBackend), which only ScapiJobsBackend implements. A supportsDeleteJobExecution() type guard lets callers narrow before calling. The Fallback Proxy detects SCAPI-only methods (those missing on OCAPI) and routes them directly to SCAPI without attempting fallback — invalid_scope errors propagate to the caller instead of trying an OCAPI that can't handle the operation. The job execution delete command now uses the type guard and gives a clear error message when the active backend can't delete. Adds 2 fallback-backend tests covering SCAPI-only method dispatch. 1732 SDK + 1219 CLI tests passing.
Replaces the layered backend abstraction (interface + adapter classes +
Proxy fallback wrapper + ScopeTierManager) for the jobs domain with a
single CLI-side dispatcher and free-function SCAPI ops. The dispatcher's
sole purpose is caching the resolved backend across multi-call
operations in apiBackend=auto mode, so a polling command (job run --wait)
doesn't re-probe SCAPI on every iteration when the user has no SCAPI
scopes provisioned.
Auth changes:
- AuthStrategy gains optional getAccessTokenForCascade(candidates).
OAuthStrategy and JwtOAuthStrategy walk candidates in order; first
that AM accepts wins, cached per requested scope set.
- findCachedTokenSatisfying scans the cache for a non-expired token
whose scopes are a superset of a required set, so a previously
granted broader-scope token can serve a narrower request without
another AM round trip.
- 5 new unit tests exercise cascade resolution, cache reuse, and error
paths.
Client/middleware changes:
- buildScapiClient gains scopeCascade option and a new
createScapiAuthMiddleware that reads the per-request
x-b2c-scope-mode header and asks the strategy to resolve the chosen
cascade tier (read or write). Legacy defaultScopes still works for
scripts/users/roles until they migrate.
- SCAPI Jobs declares its cascade once at client construction:
read = [['sfcc.jobs.rw'], ['sfcc.jobs']], write = [['sfcc.jobs.rw']].
Jobs domain:
- ScapiJobsBackend / OcapiJobsBackend / FallbackJobsBackend / DeletableJobsBackend
all deleted. ScopeTierManager dependency removed from jobs.
- New scapi-ops.ts exports free functions (scapiExecuteJob, scapiGetJobExecution,
scapiSearchJobExecutions, scapiDeleteJobExecution, scapiGetJobLog) that
take a ScapiJobsClient and declare scope mode via headers.
- mapOcapiExecution / mapOcapiSearchResult exposed as transitional helpers
for the OCAPI dispatcher branches.
- waitForJobExecution rewritten to take a getter callback over the
canonical JobExecutionInfo shape.
- New CanonicalJobExecutionError carries JobExecutionInfo (was raw OCAPI),
fixing a regression where SCAPI --wait failures reported 'ERROR'
instead of the real exit code.
CLI:
- BackendDispatcher moved to src/compat/dispatcher.ts behind a new
./compat package export. Re-exported from ./cli for ergonomics.
runScapiOnly removed; SCAPI-only commands branch on apiBackendPreference
+ buildScapiJobsClient directly.
- JobCommand exposes createJobsDispatcher, buildScapiJobsClient, and
showJobLog (canonical-first, raw-OCAPI accepted for legacy callers).
- All five job commands rewritten to dispatcher.run({scapi, ocapi})
with the scapi branch receiving a typed ScapiJobsClient.
Behavioral guarantee: no CLI-visible changes. OCAPI-only setups, explicit
--api-backend ocapi/scapi, and apiBackend=auto with full SCAPI scopes
all behave identically to the prior implementation. The auto + missing
SCAPI scopes path now caches the OCAPI choice across polls instead of
re-probing AM on every call.
13 dispatcher unit tests + 5 cascade unit tests + updated CLI command
tests. 1746 SDK + 1220 CLI tests passing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds SCAPI Admin API support to the
job,code,bm users, andbm rolescommand families, with automatic fallback to OCAPI for unprovisioned scopes. SCAPI is now the primary path; OCAPI remains as a transitional fallback for environments without SCAPI scopes provisioned in Account Manager.In
automode (default), the CLI prefers SCAPI whenshortCodeandtenantIdare configured. Oninvalid_scopefrom Account Manager, the dispatcher silently falls back to OCAPI and caches that choice for the rest of the operation — so polling commands likejob run --waitdon't re-probe AM on every iteration.Behavioral guarantee
No CLI-visible changes for any existing setup:
--api-backend ocapi: identical behavior, OCAPI runs unconditionally.--api-backend scapi: SCAPI runs; clear error if scopes are missing.autowith full SCAPI scopes: SCAPI runs and stays.autowith no SCAPI scopes: probe + cached OCAPI fallback. Output, JSON shape, and exit codes match prior behavior.What's new
b2c job execution delete— SCAPI-only (no OCAPI equivalent).--api-backendflag on instance commands (auto|scapi|ocapi), also honored fromapiBackendin dw.json.sfcc.jobs.rw/sfcc.jobs,sfcc.scripts.rw/sfcc.scripts,sfcc.users.rw/sfcc.users,sfcc.roles.rw/sfcc.roles.Architecture highlights
The jobs domain has been rebuilt around an auth-layer scope cascade primitive that the other domains will adopt in subsequent work:
AuthStrategy.getAccessTokenForCascade(candidates)— walks scope candidates against AM, caches per requested set, lets a previously granted broader-scope token satisfy a later narrower request.createScapiAuthMiddleware— picks the cascade tier (readorwrite) per operation via an internal request header.BackendDispatcher— small CLI-side primitive that routes between SCAPI ops and OCAPI free functions, caching the resolved backend for the lifetime of one logical operation. Lives atsrc/compat/dispatcher.tsbecause it's a transitional bridge that will be deleted once OCAPI is removed.scapiExecuteJob(client, ...)etc., declaring scope mode via headers. The previous five-layer abstraction (interface + adapter classes + Proxy fallback wrapper + ScopeTierManager) is gone for jobs; scripts/users/roles still use the legacy pattern until they migrate.Logging
Cascade resolution and backend fallback log at
debuglevel today. Thefalling back to OCAPIlog line is the future hook point for awarn-level migration nudge once SCAPI provisioning becomes the standard.Test plan
pnpm run typecheck:agentclean across packagespnpm run lint:agentclean across packagespnpm --filter @salesforce/b2c-tooling-sdk run test:agent(1746 passing)pnpm --filter @salesforce/b2c-cli run test:agent(1220 passing)b2c job run my-job --waitagainst an instance with SCAPI scopesb2c job execution delete my-job exec-1(SCAPI required)b2c code list,b2c bm users list,b2c bm roles listagainst both kinds of instances--api-backend scapiand--api-backend ocapiproduce expected behaviorFollow-ups (not in this PR)
code,bm users,bm rolesfrom the legacyScapi*Backend+ScopeTierManagerpattern to the new dispatcher + cascade pattern. Will deleteScopeTierManageronce all four domains migrate.debugtowarnto nudge users toward provisioning SCAPI scopes.